package com.autonavi.yunda.yunji.core.engine.script;

import com.autonavi.yunda.yunji.common.exception.AmapPreconditions;
import com.autonavi.yunda.yunji.core.engine.scriptresolver.ScriptResolver;
import com.autonavi.yunda.yunji.core.enums.EdgeType;
import com.autonavi.yunda.yunji.core.service.IInterfaceConfigService;
import com.autonavi.yunda.yunji.core.utils.ValidUtil;
import com.autonavi.yunda.yunji.core.vo.EngineData;
import com.autonavi.yunda.yunji.core.vo.EngineInterfaceConfigVO;
import com.autonavi.yunda.yunji.core.vo.ProjectExtraData;
import com.autonavi.yunda.yunji.core.vo.node.BaseNode;
import com.autonavi.yunda.yunji.core.vo.node.StartNode;
import com.autonavi.yunda.yunji.core.vo.param.NodeParam;
import com.autonavi.yunda.yunji.core.vo.param.Param;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.autonavi.yunda.yunji.core.config.EngineConstant.*;

/**
 * @author cizhong.wcs
 * @date 2022/6/23 11:24
 * <p>
 * 接口脚本管理器
 */
@Slf4j
@Component
public class ScriptClassManager {
    private static final String END = "end";
    private static final String START = "start";
    private static final Logger SCRIPT_LOGGER = LoggerFactory.getLogger("SCRIPT_LOG");
    @Resource
    private IInterfaceConfigService interfaceConfigService;

    private final Map<String, InterfaceScriptClass> scriptCacheMap = new ConcurrentHashMap<>();

    public InterfaceScriptClass fetchScriptClassByUriFromCache(String uri) {
        return scriptCacheMap.get(uri);
    }

    /**
     * 强制刷新接口的缓存
     */
    public void refreshScriptClassCacheByUri(String uri) {
        log.warn("refreshScriptClassCacheByUri start uri = {}", uri);
        EngineInterfaceConfigVO interfaceConfigVO = interfaceConfigService.fetchCurrentValidScriptVO(uri);
        if (interfaceConfigVO == null) {
            log.warn("refreshScriptClassCacheByUri uri:{} is not published or gray in this machine", uri);
            scriptCacheMap.remove(uri);
            return;
        }
        InterfaceScriptClass scriptClass = generateInterfaceScriptClass(interfaceConfigVO);
        scriptCacheMap.put(uri, scriptClass);
        log.warn("refreshScriptClassCacheByUri end uri = {} ", uri);

    }

    public static InterfaceScriptClass generateInterfaceScriptClass(EngineInterfaceConfigVO vo) {
        // 创建engineData对象（整个接口的节点信息）
        EngineData engineData = new EngineData(vo.getEngineData());
        // 校验链路合法性
        checkEngineDataLink(engineData);
        ProjectExtraData projectExtraData = vo.getProjectExtraData();
        // 解析接口主脚本
        String script = ScriptResolver.resolveInterfaceScript(engineData, vo.getProjectId(), projectExtraData.responseFormat);
        SCRIPT_LOGGER.debug("generateInterfaceScriptClass id = {}, uri = {}, script :\n=========\n{}\n=========", vo.getId(), vo.getUri(), script);
        // 创建InterfaceScriptClass对象
        InterfaceScriptClass scriptClass = new InterfaceScriptClass(script, vo, engineData, projectExtraData);
        // 解析节点和边中条件单独的脚本
        Map<String, IFunctionScript> functionScriptClassMap = ScriptResolver.resolveNodesToFunctionScriptMap(engineData);
        scriptClass.functionScriptClassMap.putAll(functionScriptClassMap);
        return scriptClass;
    }

    public void checkAndRefreshScriptClassManager() {
        //更新缓存中已有的uri
        scriptCacheMap.forEach((uri, cacheScriptClass) -> {
            EngineInterfaceConfigVO dbInterfaceConfig = interfaceConfigService.fetchCurrentValidScriptVO(uri);
            boolean dbCacheNotConsistent =
                    (dbInterfaceConfig != null && cacheScriptClass != null
                            //id不同或更新时间不同
                            && (!dbInterfaceConfig.getId().equals(cacheScriptClass.interfaceId) || !dbInterfaceConfig.getUpdateTime().equals(cacheScriptClass.updateTime))
                    )
                            || (dbInterfaceConfig == null && cacheScriptClass != null)
                            || (dbInterfaceConfig != null && cacheScriptClass == null);
            if (dbCacheNotConsistent) {
                log.info("dbCacheNotConsistent uri need refresh, uri : {}", uri);
                try {
                    refreshScriptClassCacheByUri(uri);
                } catch (Exception e) {
                    log.error("checkAndRefreshScriptClassManager error  uri : {}", uri, e);
                }

            }
        });
        //更新缓存中没有的uri
        Set<String> newPublishedUris = new HashSet<>(interfaceConfigService.listNewPublishedUris(scriptCacheMap.keySet()));
        log.warn("新增uri list = {}", newPublishedUris);
        newPublishedUris.forEach(uri -> {
            try {
                refreshScriptClassCacheByUri(uri);
            } catch (Exception e) {
                log.error("checkAndRefreshScriptClassManager error  uri :{}", uri, e);
            }
        });
    }

    /**
     * 检测链路是否有环连接及尾节点是否为end节点及链路参数校验
     */
    public static void checkEngineDataLink(EngineData engineData) {

        List<BaseNode> nodes = engineData.nodes;
        // 链路中不能存在名称相同的节点
        int distinctNodeCount = (int) nodes.stream().map(node -> node.name).distinct().count();
        AmapPreconditions.checkArgumentForUser(distinctNodeCount == nodes.size(), "不允许重复名字节点");
        // 找到每个节点的上下游节点，下游节点存在nextStreamMap，上游节点存在preStreamMap
        Map<String, ArrayList<String>> nextStreamMap = new HashMap<>(16);
        Map<String, ArrayList<String>> preStreamMap = new HashMap<>(16);
        engineData.edges.forEach(edge -> {
            AmapPreconditions.checkArgumentForUser(!START.equalsIgnoreCase(edge.tail), "start节点不允许存在上游连接节点");
            if (null != nextStreamMap.get(edge.head)) {
                nextStreamMap.get(edge.head).add(edge.tail);
            } else {
                nextStreamMap.put(edge.head, Lists.newArrayList(edge.tail));
            }
            if (null != preStreamMap.get(edge.tail)) {
                preStreamMap.get(edge.tail).add(edge.head);
            } else {
                preStreamMap.put(edge.tail, Lists.newArrayList(edge.head));
            }
        });

        Set<String> totalLinkNodeNames = new HashSet<>();//存放成链的节点名称（去除不在链中的节点名称）
        List<String> linklist = new ArrayList<>();
        Map<String, BaseNode> nodeMap = nodes.stream().collect(Collectors.toMap(baseNode -> baseNode.name, Function.identity()));
        linklist.add(START);
        // 校验链路是否存在闭环
        checkLinkCycle(START, nextStreamMap, totalLinkNodeNames, linklist, nodeMap);
        // 移除无关的节点，只剩下和链路相关的节点
        removeUselessNodesAndEdges(totalLinkNodeNames, engineData);
        // 校验各个节点的参数（不校验是否存在）
        checkParams(engineData);
        // 节点间参数依赖（借助队列进行递归，真他妈帅）
        checkParamsDepend(totalLinkNodeNames, preStreamMap, engineData, nodeMap);
    }

    private static void removeUselessNodesAndEdges(Set<String> totalLinkNodeNames, EngineData engineData) {
        engineData.nodes = engineData.nodes.stream().filter(node -> totalLinkNodeNames.contains(node.name)).collect(
                Collectors.toList());
        engineData.edges = engineData.edges.stream().filter(edge -> totalLinkNodeNames.contains(edge.head) && totalLinkNodeNames.contains(edge.tail)).collect(
                Collectors.toList());
    }

    /**
     * 检查是否存在闭环
     * @param curNodeName 当前节点名称
     * @param downStreamMap 当前节点名称_下级节点名称集合 map
     * @param totalLinkNodeNames 成链的节点名称集合（去除不成链的节点名称）
     * @param linkList 用来检测是否存在闭环的中间列表（如果存在闭环，那么递归遍历一圈，一定会存在同名的元素）
     * @param nodeMap 节点名称_节点对象 map
     */
    public static void checkLinkCycle(String curNodeName, Map<String, ArrayList<String>> downStreamMap, Set<String> totalLinkNodeNames, List<String> linkList,
                                      Map<String, BaseNode> nodeMap) {
        AmapPreconditions.checkArgumentForUser(null != nodeMap.get(curNodeName), curNodeName + "节点不存在");
        ArrayList<String> listDown = downStreamMap.get(curNodeName);
        // 如果为null， 说明不存在下级节点
        if (null == listDown) {
            // 如果下级节点为null， 正常情况一定是end节点，如果不是抛出异常
            AmapPreconditions.checkArgumentForUser(END.equals(curNodeName), curNodeName + "存在不以end为节点结尾的链路，尾节点为：" + curNodeName);
            // 存入成链的节点名称
            totalLinkNodeNames.addAll(linkList);
            return;
        }
        // 遍历所有的下级节点
        for (String nextNodeName : listDown) {
            AmapPreconditions.checkArgumentForUser(!linkList.contains(nextNodeName), nextNodeName + "节点 存在环连接的链路");
            linkList.add(nextNodeName);
            checkLinkCycle(nextNodeName, downStreamMap, totalLinkNodeNames, linkList, nodeMap);
            linkList.remove(linkList.size() - 1);
        }
    }

    /**
     * 检查入参和节点参数,边中参数的合法性
     */
    public static void checkParams(EngineData engineData) {
        StartNode startNode = engineData.fetchStartNode();
        startNode.inputParams.forEach(param -> {
            AmapPreconditions.checkArgumentForUser(StringUtils.isNoneBlank(param.name), "start节点入参中参数名称不能为空");
            AmapPreconditions.checkArgumentForUser(
                    ValidUtil.checkRegexAndReturn(param.name, NODE_PARAM_NAME_REGEX),
                    "start节点参数名" + param.name + "只能使用[字母,数字,-,_]且不能以数字开头");
        });
        engineData.engineConfig.initParams.forEach(param -> {
            AmapPreconditions.checkArgumentForUser(StringUtils.isNoneBlank(param.name), "全局配置中初始化参数名称不能为空");
            AmapPreconditions.checkArgumentForUser(
                    ValidUtil.checkRegexAndReturn(param.name, GROOVY_SYNTAX_VARIABLE_REGEX),
                    "全局配置中初始化参数名" + param.name + "只能使用[字母,数字,_]且不能以数字开头");
        });
        AmapPreconditions.checkArgumentForUser(
                startNode.inputParams.stream().map(Param::getName).distinct().count() == startNode.inputParams.size(),
                "start节点入参列表不允许存在相同名称的参数"
        );
        AmapPreconditions.checkArgumentForUser(
                engineData.engineConfig.initParams.stream().map(NodeParam::getName).distinct().count() == engineData.engineConfig.initParams.size(),
                "初始化参数列表不允许存在相同名称的参数"
        );
        engineData.nodes.forEach(BaseNode::checkParam);
        engineData.edges.stream().filter(e -> EdgeType.custom.equals(e.type)).forEach(BaseNode::checkParam);
    }

    /**
     * 检查节点间参数依赖
     */
    public static void checkParamsDepend(Set<String> totalLinkNodeNames, Map<String, ArrayList<String>> preStreamMap, EngineData engineData, Map<String, BaseNode> nodeMap) {
        // 遍历全部节点名称
        totalLinkNodeNames.forEach(nodeName -> {
            if (START.equals(nodeName)) {
                return;
            }
            // 递归找到当前节点所有允许的参数名称（当前节点所有的前节点、开始节点的传参都可以当做参数）
            Set<String> preNodeVarSet = getPreNodeVarList(preStreamMap, nodeName, nodeMap, engineData);
            // 如果是end节点，判断所有的输出参数是否在允许参数集合中
            if (END.equals(nodeName)) {
                engineData.fetchEndNode().outputParams.forEach(
                        (paramName, valueRef) -> checkValueRef(valueRef, preNodeVarSet, nodeName, paramName)
                );
            }
            // 如果是其他节点，判断参数列表是否在允许参数列表内
            else {
                nodeMap.get(nodeName).params.forEach(param -> checkValueRef(param.dataSource, preNodeVarSet, nodeName, param.name));
            }
        });
    }

    private static void checkValueRef(String valueRef, Set<String> preNodeVarSet, String nodeName, String paramName) {
        if (CONSTANT_VALUE_REF.equals(valueRef) || DYNAMIC_CONFIG_REF.equals(valueRef) || valueRef.startsWith(CONTEXT_PARAM_NAME)) {
            return;
        }
        String var = valueRef.contains(".") ? valueRef.split("\\.")[0] : valueRef;
        var = var.replaceAll("\\[.*", "");
        AmapPreconditions.checkArgumentForUser(preNodeVarSet.contains(var), nodeName + "节点:'" + paramName + "'参数的引用来源'" + var + "'无声明");
    }

    /**
     * 获取上游节点输出结果映射
     */
    public static Set<String> getPreNodeVarList(Map<String, ArrayList<String>> preStreamMap, String currentNodeName, Map<String, BaseNode> nodeMap, EngineData engineData) {
        Set<String> nodeVarSet = new HashSet<>();
        Queue<String> queue = new LinkedList<>();
        queue.offer(currentNodeName);
        while (!queue.isEmpty()) {
            String nodeName = queue.poll();
            ArrayList<String> tempListPre = preStreamMap.get(nodeName);
            if (tempListPre != null) {
                tempListPre.forEach(preName -> {
                    if (!START.equalsIgnoreCase(preName)) {
                        nodeVarSet.add(nodeMap.get(preName).var);
                        queue.offer(preName);
                    }
                });

            }
        }
        StartNode startNode = engineData.fetchStartNode();
        Set<String> inputParamSet = startNode.inputParams.stream().map(t -> t.name).collect(Collectors.toSet());
        Set<String> initParamSet = engineData.engineConfig.initParams.stream().map(t -> t.name).collect(Collectors.toSet());
        nodeVarSet.addAll(inputParamSet);
        nodeVarSet.addAll(initParamSet);
        return nodeVarSet;
    }

}
